﻿
namespace Patterns4Net.ArchitectureExplorer.Diagram.Services.Relations
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.IO;
    using System.Linq;
    using System.Diagnostics.Contracts;
    using Caliburn.Micro;
    using Mono.Cecil;
    using Patterns4Net.ArchitectureExplorer.Infrastructure;
    using Patterns4Net.Attributes.DesignPatterns;
    using Patterns4Net.Attributes.Relations;
    using Patterns4Net.Core.CecilExtensions;

    public abstract class RelationsProvider : DiagramVisitor, IDiagramUpdater
    {
        protected RelationsProvider()
        {
            this.Logger = new NullLogger();
        }

        [Import]
        public ILog Logger { get; set; }

        public void UpdateDiagram(Diagram diagramToUpdate, TextWriter writer)
        {
            this.Output = writer;
            this.Diagram = diagramToUpdate;
            this.Diagram.Accept(this);
        }

        protected TextWriter Output { get; private set; }

        protected Diagram Diagram { get; private set; }

        [TemplateMethod]
        public override void Visit(TypeInfo typeInfo)
        {
            var typeDefinition = typeInfo.GetTag<TypeDefinition>();
            if (typeDefinition == null)
            {
                return;
            }

            try
            {
                this.Visit(typeInfo, typeDefinition);
            }
            catch (Exception ex)
            {
                this.Output.WriteLine(
                    "Diagram updated {0} cannot process type {1}. " +
                    "This may happen when type {1} contains some MSIL constructs that {0} does not support. " +
                    "Detailed description has been logged.",
                    this.GetType().Name,
                    typeDefinition.FullName);
                this.Logger.Error(ex);
            }
        }

        protected abstract void Visit(TypeInfo typeInfo, TypeDefinition typeDefinition);

        protected abstract Relation CreateRelation(DiagramElement source, DiagramElement target, Cardinality cardinality);

        protected abstract bool IsMadeObsoleteBy(Relation relation);

        protected Relation AddRelationIfExists(TypeInfo source, TypeAndCardinality typeAndCardinality)
        {
            return AddRelationIfExists(source, typeAndCardinality.Type, typeAndCardinality.Cardinality);
        }

        [TemplateMethod]
        protected Relation AddRelationIfExists(TypeInfo source, TypeReference target, Cardinality cardinality = Cardinality.One)
        {
            Contract.Requires(source != null);
            Contract.Requires(target != null);
            var targetTypeInfo = this.Diagram.Types.FirstOrDefault(
                x =>
                    {
                        var typeDef = x.GetTag<TypeDefinition>();
                        return typeDef != null && typeDef.IsEqual(target);
                    });

            if (targetTypeInfo != null)
            {
                var relation = this.CreateRelation(source, targetTypeInfo, cardinality);

                if (this.Diagram.Relations.Any(r => r.Source == source && r.Target == targetTypeInfo && this.IsMadeObsoleteBy(r)))
                {
                    // we dont add duplicate relations.
                    return null;
                }

                if (this.Diagram.Relations.Contains(relation) == false)
                {
                    this.Diagram.AddRelation(relation);
                    return relation;
                }
            }

            return null;
        }

        protected TypeAndCardinality GetTypeAndCardinality(FieldReference field)
        {
            var type = field.FieldType;
            var genericArgs = type.GetGenericArgumentsFor(typeof(IEnumerable<>));

            if (genericArgs.Any())
            {
                return new TypeAndCardinality { Type = genericArgs.First(), Cardinality = Cardinality.Multiple };
            }
            else
            {
                return new TypeAndCardinality { Type = type, Cardinality = Cardinality.One };
            }
        }

        protected class TypeAndCardinality
        {
            public TypeReference Type { get; set; }

            public Cardinality Cardinality { get; set; }
        }
    }
}
